package kit.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import kit.persistency.KitDB;

/**
 * Libreria a supporto dell'interfaccia grafica. Tramite i metodi sottostanti
 * sono invocati processi esterni (come GPG o sha512) e mantenute le
 * informazioni non strettamente attinenti all'algoritmo di Ellison -mantenute
 * invece nella classe {@link Challenge}-.
 * 
 * @author mattia
 */
public class Core {
	/**
	 * In base alle preferenze dell'utente, nel campo sarà mantenuta un'instanza
	 * di tipo {@link iEntropyCalculator}.
	 * 
	 * @see {@link ToZeroEntropyCalculator}
	 * @see {@link ToQuarterRootEntropyCalculator}
	 * @see {@link ToHalfEntropyCalculator}
	 */
	private iEntropyCalculator calc;

	/**
	 * Verifica in corso.
	 */
	private Challenge currentChallenge;

	/**
	 * Ultima domanda inviata alla controparte.
	 */
	private String currentQuestion;

	/**
	 * Ultima risposta ottenuta dalla controparte.
	 */
	private String currentAnswer;

	private HashMap<String, List<String>> usedAnswers;

	/**
	 * Costruttore vuoto e privato, per evitare errori di distrazione in fase di
	 * sviluppo. Ogni invocazione di una funzionalità della classe è effettuata
	 * grazie al metodo obtainSingleton.
	 * 
	 * @see {@link Core#obtainSingleton()}
	 */
	private Core() {
		usedAnswers = new HashMap<>();
	}

	/**
	 * I metodi runShellCommand, nelle seguenti definizioni, permettono
	 * l'esecuzione di comandi sul terminale.
	 * 
	 * @param command
	 *            Specifica il comando da eseguire.
	 * @return Restituisce le stringhe apparse sullo <i>standard output</i>.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 *             Se l'esecuzione del comando produce dei messaggi sullo
	 *             <i>standard error</i>.
	 */
	private List<String> runShellCommand(String command) throws IOException,
			InterruptedException, ShellException {
		return runShellCommand(command, true);
	}

	/**
	 * @param command
	 * @param verbose
	 *            In funzione del valore, viene analizzato il contenuto dello
	 *            <i>standard error</i> e di conseguenza trattate le eccezioni
	 *            di tipo ShellException.
	 * @return
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	private List<String> runShellCommand(String command, boolean verbose)
			throws IOException, InterruptedException, ShellException {
		List<String> output = new LinkedList<>();
		String[] cmd = { "/bin/sh", "-c", command };

		Process p = Runtime.getRuntime().exec(cmd);
		int result = p.waitFor();

		BufferedReader stdInput = new BufferedReader(new InputStreamReader(
				p.getInputStream()));

		BufferedReader stdError = new BufferedReader(new InputStreamReader(
				p.getErrorStream()));

		String line;
		while ((line = stdInput.readLine()) != null) {
			output.add(line);
		}

		if (verbose) {
			String error = "";
			while ((line = stdError.readLine()) != null) {
				error += line + "\n";
			}
			if (!error.equals("")) {
				throw new ShellException(error);
			}
		}
		if (result != 0)
			throw new ShellException("Error executing: " + command);
		return output;
	}

	/**
	 * Verifica la risposta ottenuta dalla controparte con le risposte.
	 * 
	 * @param answer
	 *            Risposta in chiaro.
	 * @param hashedAnswer
	 *            Digest della risposta della controparte.
	 * @return Restituisce true se la risposta ottenuta dal controparte coincide
	 *         con l'hash effettuato sul parametro answer.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public boolean checkAnswer(String answer, String hashedAnswer)
			throws IOException, InterruptedException, ShellException {
		return buildAnswer(answer).equals(hashedAnswer);
	}

	/**
	 * Il metodo concatena il testo della risposta con la chiave pubblica
	 * dell'utente locale e la chiave pubblica della controparte facendone poi
	 * l'hash. Viene utilizzato il processo sha512 per ottenere il risultato.
	 * 
	 * @param answer
	 *            Risposta di cui si vuole effettuare l'hash.
	 * @return Digest dell'operazione di hash, secondo il formato proposta da
	 *         Ellison. Digest = Sha512(Risposta | KP_Locale | KP_Controparte)
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public String buildAnswer(String answer) throws IOException,
			InterruptedException, ShellException {
		String result = runShellCommand(
				String.format(
						"(echo -n %s && gpg2 --armor --export '%s' && gpg2 --armor --export '%s') | sha512sum",
						answer, currentChallenge.getLocalUID(),
						currentChallenge.getReceiverUID())).get(0);
		return trimHashResult(result);
	}

	/**
	 * Dato il formato dei digest di Sha512, vengono rimossi alcuni caratteri
	 * finali.
	 * 
	 * @param digest
	 * @return Digest paragonabile all'originale, privato degli spazi bianchi e
	 *         del carattere '-' finale.
	 */
	private String trimHashResult(String digest) {
		return digest.substring(0, digest.length() - 2).trim();
	}

	/**
	 * Sfruttando GPG sono ottenuti gli UID ci cui si possiede la chiave
	 * pubblica.
	 * 
	 * @return Lista degli utenti di cui si possiede una chiave pubblica.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public List<String> getPublicKeysUID() throws IOException,
			InterruptedException, ShellException {
		return getGPGKeys(true);
	}

	/**
	 * Sfruttando GPG sono ottenuti gli UID ci cui si possiede la chiave
	 * privata.
	 * 
	 * @return Lista degli utenti di cui si possiede una chiave privata.
	 * @return
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public List<String> getPrivateKeysUID() throws IOException,
			InterruptedException, ShellException {
		return getGPGKeys(false);
	}

	/**
	 * In funzione del parametro arePublicKeysNeeded si ottiene l'elenco delle
	 * chiavi pubbliche o private di cui si dispone. Il risultato prodotto da
	 * GPG viene poi formattato per semplificarne l'utilizzo nella GUI.
	 * 
	 * @param arePublicKeysNeeded
	 * @return Lista degli utenti di cui si possiede la chiave pubblica o
	 *         privata.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public List<String> getGPGKeys(boolean arePublicKeysNeeded)
			throws IOException, InterruptedException, ShellException {
		String keyType = arePublicKeysNeeded ? "public" : "secret";
		List<String> output = runShellCommand(String.format(
				"gpg2 --list-%s-keys | grep uid", keyType));
		List<String> filteredUID = new LinkedList<>();
		for (String uid : output) {
			uid = uid.split("uid")[1];
			uid = uid.trim();
			filteredUID.add(uid);
		}
		return filteredUID;
	}

	/**
	 * Codifica il testo del messaggio sfruttando GPG. Richiede l'inserimento
	 * della passphrase associata alla chiave privata.
	 * 
	 * @param message
	 *            Messaggio in chiaro di cui si vuole effettuare la codifica.
	 * @param output
	 *            File sul quale salvare il risultato dell'operazione.
	 * @return Path del file sul quale è stato rediretto il risultato
	 *         dell'operazione.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public String encode(String message, File output) throws IOException,
			InterruptedException, ShellException {

		String command = String.format(
				"echo -n %s | gpg2 -e -u \"%s\" -r \"%s\" > %s", message,
				currentChallenge.getLocalUID(),
				currentChallenge.getReceiverUID(), output.getAbsolutePath());
		runShellCommand(command);
		return output.getName();
	}

	/**
	 * Decodifica il testo del messaggio criptato sfruttando GPG. Richiede
	 * l'inserimento della passphrase associata alla chiave privata.
	 * 
	 * @param fileName
	 *            File sul quale salvare il risultato dell'operazione.
	 * @return Restituisce il messaggio decodificato.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public String decode(String fileName) throws IOException,
			InterruptedException, ShellException {
		File input = new File(fileName);

		return runShellCommand(
				String.format("gpg2 --no-tty -d %s", input.getAbsolutePath()),
				false).get(0);
	}

	/**
	 * Decodifica il testo criptato mantenuto su due file separati sfruttando
	 * GPG. Richiede l'inserimento della passphrase associata alla chiave
	 * privata.
	 * 
	 * @param firstFilePath
	 *            Primo metà del messaggio criptato.
	 * @param secondFilePath
	 *            Primo metà del messaggio criptato.
	 * @return Restituisce il messaggio decodificato.
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	public String decode(String firstFilePath, String secondFilePath)
			throws IOException, InterruptedException, ShellException {

		return runShellCommand(
				String.format("cat %s %s | gpg2 --no-tty -d", firstFilePath,
						secondFilePath), false).get(0);
	}

	/**
	 * @return Restituisce vero sse è in corso una verifica.
	 */
	public boolean isChallengeRunning() {
		return currentChallenge != null;
	}

	public Challenge getCurrentChallenge() {
		return currentChallenge;
	}

	/**
	 * Inizio di una nuova verifica, calcolando dinamicamente il valore di
	 * entropia da raggiungere nel corso del processo.
	 * 
	 * @param nRound
	 *            Limite di fallimenti massimi.
	 * @param localUID
	 *            ID GPG dell'utente locale.
	 * @param receiverUID
	 *            ID GPG dell'utente remoto.
	 * @throws ShellException
	 * @throws InterruptedException
	 * @throws IOException
	 */
	public void setNewChallenge(int nRound, String localUID, String receiverUID) {
		double entropyLimit;
		try {
			entropyLimit = calculateEntropyLimit(localUID);
		} catch (IOException | InterruptedException | ShellException e) {
			entropyLimit = 512;
		}
		currentChallenge = new Challenge(nRound, localUID, receiverUID,
				entropyLimit);
	}

	/**
	 * Calcola il valore minimo di entropia da raggiungere attraverso le
	 * iterazioni. Indicato sull'articolo con la variabile S, è dato dal minore
	 * fra i parametri di sicurezza utilizzati dalla funzione di hash e di
	 * codifica.
	 * 
	 * @param localUID
	 *            Utente del quale verificare la lunghezza della chiave.
	 * @return minimo(|Sha512|,|Pk|)
	 * @throws IOException
	 * @throws InterruptedException
	 * @throws ShellException
	 */
	private double calculateEntropyLimit(String localUID) throws IOException,
			InterruptedException, ShellException {
		int hashSecurityParameter = 512;
		int encryptionSecurityParameter = 0;

		for (String line : runShellCommand(String.format(
				"gpg2 --armor --export '%s'", localUID))) {
			encryptionSecurityParameter += line.length();
		}
		return Math.min(hashSecurityParameter, encryptionSecurityParameter);
	}

	/**
	 * Calcolo del valore di entropia guadagnato a fine round. NB: In un
	 * scenario a bassa entropia, come ad esempio in fase di debug, non è
	 * soddisfatta l'ipotesi I << 2^Ea (dove I rappresenta il numero di risposte
	 * per la domanda effettuata e Ea il valore cumulativo dell'entropia di
	 * tutte le risposte alla domanda); viene quindi applicato il modulo. Viene
	 * inoltre aggiunta la coppia <domanda, risposta> alla tabella dei segreti
	 * di cui potenzialmente l'attaccante è giunto a conoscenza.
	 * 
	 * @return Il punteggio del round, dato dalle formule presenti nel pt. 6 di
	 *         pagina 8.
	 */
	public double calculateRoundEntropy() {
		List<String> correctAnswers = usedAnswers.get(currentQuestion);
		if (correctAnswers == null) {
			correctAnswers = new LinkedList<String>();
			correctAnswers.add(currentAnswer);
		} else if (!correctAnswers.contains(currentAnswer)) {
			correctAnswers.add(currentAnswer);
		}
		usedAnswers.put(currentQuestion, correctAnswers);

		double P = 0.0, result;
		KitDB db = new KitDB();
		for (String answer : db.getAnswers(currentQuestion)) {
			double e = db.getAnswerEntropy(currentQuestion, answer);
			if (e == 0)
				continue;

			P += Math.pow(2, -e);
		}
		result = -(Math.log(P) / Math.log(2));
		System.err.println("Round score: " + result);
		return result < 0 ? Math.abs(result) : result;
	}

	/**
	 * Separa l'output presente su un file in due metà, per favorire l'algoritmo
	 * di intelocking proposto da Rivest e Shamir.
	 * 
	 * @param firstHalfFile
	 *            Primo file di output.
	 * @param secondHalfFile
	 *            Secondo file di output.
	 * @param input
	 *            File originale che è necessario separare.
	 * @throws IOException
	 */
	public void outputSplitter(File firstHalfFile, File secondHalfFile,
			File input) throws IOException {
		int half = byteCounter(input) / 2;
		FileInputStream fis = new FileInputStream(input);
		FileOutputStream fos1 = new FileOutputStream(firstHalfFile);
		FileOutputStream fos2 = new FileOutputStream(secondHalfFile);

		int b, i = 0;
		while ((b = fis.read()) != -1) {
			if (i < half)
				fos1.write(b);
			else
				fos2.write(b);
			i++;
		}

		fis.close();
		fos1.close();
		fos2.close();
	}

	/**
	 * @param file
	 *            File del quale effettuare la conta dei byte.
	 * @return Il numero di caratteri presenti.
	 * @throws IOException
	 */
	private int byteCounter(File file) throws IOException {
		FileInputStream fis = new FileInputStream(file);
		int count = 0;
		while (fis.read() != -1)
			count++;
		fis.close();
		return count;
	}

	/**
	 * Date le domande a cui è stata ottenuta risposta corretta durante la
	 * verifica, in caso di fallimento del protocollo queste vengono impostate
	 * come esposte ed è corretta loro l'entropia.
	 * 
	 * @return Ritorna vero sse la procedura è andata a buon fine, falso
	 *         altrimenti.
	 */
	public boolean setAnswersExposed() {
		KitDB db = new KitDB();
		for (String question : usedAnswers.keySet()) {
			for (String answer : usedAnswers.get(question)) {
				if (!db.setAnswerExposed(question, answer, true, getCalc()))
					return false;
			}
		}
		return true;
	}

	/**
	 * Conclude la verifica in corso.
	 */
	public void endCurrentChallenge() {
		currentChallenge = null;
	}

	public void setCalc(iEntropyCalculator calc) {
		this.calc = calc;
	}

	public iEntropyCalculator getCalc() {
		return calc;
	}

	public void setCurrentQuestion(String question) {
		currentQuestion = question;
	}

	public String getCurrentQuestion() {
		return currentQuestion;
	}

	public String getCurrentAnswer() {
		return currentAnswer;
	}

	public void setCurrentAnswer(String currentAnswer) {
		this.currentAnswer = currentAnswer;
	}

	public String getReceiverUID() {
		return currentChallenge != null ? currentChallenge.getReceiverUID()
				: null;
	}

	public String getLocalUID() {
		return currentChallenge != null ? currentChallenge.getLocalUID() : null;
	}

	public void setLocalUID(String localUID) {
		currentChallenge.setLocalUID(localUID);
	}

	public void setReceiverUID(String receiverUID) {
		currentChallenge.setReceiverUID(receiverUID);
	}

	private static Core singleton;

	/**
	 * Restituisce il singleton.
	 */
	public static Core obtainSingleton() {
		return singleton != null ? singleton : (singleton = new Core());
	}
}
